/*
 * Iparelan ExcepGen
 * This file is part of ExcepGen.
 * Copyright (c) 2008, 2009, Iparelan Solutions, LLC. All rights reserved.
 *
 * ExcepGen is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * ExcepGen is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ExcepGen. If not, see <http://www.gnu.org/licenses/>.
 */

package com.iparelan.excepgen;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import java.util.Collection;
import java.util.HashSet;

import java.util.concurrent.atomic.AtomicBoolean;

import java.util.logging.Logger;

import static java.util.logging.Level.FINE;

import net.jcip.annotations.NotThreadSafe;

import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import org.xml.sax.helpers.DefaultHandler;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.Nullable;

import static com.iparelan.excepgen.ExcepGen.LINE_SEPARATOR;

import static com.iparelan.excepgen.L7dMsg.MSGS;

/**
 * A SAX event handler that interprets the ExcepGen configuration syntax.
 *
 * @author Greg Mattes
 * @version September 2008
 */
@NotThreadSafe
@SuppressWarnings("PMD.TooManyMethods") // justification: constant specific enum behaviors cause method threshold to be exceeded
final class ConfigHandler extends DefaultHandler {
    /** The ExcepGen {@link java.util.logging.Logger}. */
    static final Logger LOGGER = Logger.getLogger(ExcepGen.class.getSimpleName());

    /**
     * Instances of this class are considered to be initialized after
     * receiving exactly one {@link org.xml.sax.Locator}.
     */
    private final AtomicBoolean initialized = new AtomicBoolean();

    /**
     * Holds the {@code ExceptionSpecification}s read from the
     * configuration file.
     */
    private final Collection<ExceptionSpecification> exceptions = new HashSet<ExceptionSpecification>();

    /**
     * Records parsing location.
     *
     * @to.do
     *
     * <ul>
     *
     * <li>Create a {@code QuasiFinal} annotation in the general Iparelan
     *     tools, make those tools available here. The annotation parameter
     *     specifies the member that ensures single initialization.
     *
     * </ul>
     */
    //@QuasiFinal("initialized")
    private Locator locator;

    /**
     * The default package name for {@code ExceptionSpecification}s if no
     * particular package is given.
     */
    @SuppressWarnings("PMD.SingularField") // justification: maintains state during SAX parsing
    private String defaultPackageName;

    /**
     * The documentation to generate at the beginning of each exception
     * source code file.
     */
    @SuppressWarnings("PMD.SingularField") // justification: maintains state during SAX parsing
    private String documentationHeader;

    /**
     * The Javadoc license information placed at the beginning of each
     * exception source code file after the {@link #documentationHeader}.
     */
    @SuppressWarnings("PMD.SingularField") // justification: maintains state during SAX parsing
    private String license;

    /**
     * The {@code ExceptionSpecification} currently being built as parsing
     * proceeds.
     *
     * <p>The value of this variable alternates between being {@code null} and
     * successive exception specifications during configuration file parsing.
     */
    @CheckForNull
    //@Nullable TODO
    @edu.umd.cs.findbugs.annotations.SuppressWarnings(
        value = "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR",
        justification = "(false positive): default value of null is the intended initialization value"
    )
    private ExceptionSpecification currentException;

    /**
     * Whether additional exception code is being parsed.
     */
    private boolean readingCode;

    /** The XML elements of a ExcepGen configuration file. */
    private enum Element {
        /** The {@code throwables} XML element. */
        THROWABLES {
            @Override
            protected void onStart(final ConfigHandler configHandler, final Attributes attributes) throws SAXException {
                String licenseFilename = null;
                try {
                    final String packageAttribute = ThrowablesAttributes.PACKAGE.toString();
                    configHandler.defaultPackageName = attributes.getValue(packageAttribute);

                    final String headerAttribute = ThrowablesAttributes.HEADER.toString();
                    configHandler.documentationHeader = attributes.getValue(headerAttribute);

                    final String licenseAttribute = ThrowablesAttributes.LICENSEFILE.toString();
                    licenseFilename = attributes.getValue(licenseAttribute);
                    configHandler.license = (licenseFilename == null) ? "" : configHandler.getFileContentsAsJavadoc(licenseFilename);
                } catch (final IOException ioe) {
                    final String errMsg = MSGS.get(L7dMsg.IO_ERROR, licenseFilename);
                    final SAXException se = new SAXParseException(errMsg, configHandler.locator, ioe);
                    se.initCause(ioe);
                    throw se; // SuppressWarnings(PMD.PreserveStackTrace) justification: (false positive) exceptions are chained
                }
            }
        },

        /** The {@code runtimeexception} XML element. */
        RUNTIMEEXCEPTION {
            @Override
            protected void onStart(final ConfigHandler configHandler, final Attributes attributes) {
                final String defaultClassName = RuntimeException.class.getSimpleName();
                configHandler.newExceptionSpecification(attributes, defaultClassName);
            }

            @Override
            protected void onEnd(final ConfigHandler configHandler) {
                configHandler.clearExceptionSpecification();
            }
        },

        /** The {@code exception} XML element. */
        EXCEPTION {
            @Override
            protected void onStart(final ConfigHandler configHandler, final Attributes attributes) {
                final String defaultClassName = Exception.class.getSimpleName();
                configHandler.newExceptionSpecification(attributes, defaultClassName);
            }

            @Override
            protected void onEnd(final ConfigHandler configHandler) {
                configHandler.clearExceptionSpecification();
            }
        },

        /** The {@code subtypes} XML element. */
        SUBTYPES,

        /** The {@code subtype} XML element. */
        SUBTYPE {
            @Override
            protected void onStart(final ConfigHandler configHandler, final Attributes attributes) {
                final String nameAttribute = SubTypeAttributes.NAME.toString();
                final String name = attributes.getValue(nameAttribute);
                final String nameUpperCase = name.toUpperCase();
                configHandler.currentException.addSubTypeName(nameUpperCase);
            }
        },

        /** The {@code code} XML element. */
        CODE {
            @Override
            protected void onStart(final ConfigHandler configHandler, final Attributes attributes) {
                configHandler.readingCode = true;
            }

            @Override
            protected void onEnd(final ConfigHandler configHandler) {
                configHandler.readingCode = false;
            }
        };

        /**
         * Behavior invoked when an element-start event is detected by a SAX
         * parser for this element.
         *
         * @param configHandler
         *
         *        The {@link ConfigHandler} that provides the parse state that
         *        may be manipulated by this behavior. May not be {@code
         *        null}.
         *
         * @param attributes
         *
         *        The XML attributes present for the element. May not be
         *        {@code null}.
         *
         * @throws SAXException
         *
         *         If some error occurs during behavior execution.
         */
        protected void onStart(final ConfigHandler configHandler, final Attributes attributes) throws SAXException {
            // default behavior is to do nothing
        }

        /**
         * Behavior invoked when an element-end event is detected by a SAX
         * parser for this element.
         *
         * @param configHandler
         *
         *        The {@link ConfigHandler} that provides the parse state that
         *        may be manipulated by this behavior. May not be {@code
         *        null}.
         *
         * @throws SAXException
         *
         *         If some error occurs during behavior execution.
         */
        protected void onEnd(final ConfigHandler configHandler) throws SAXException {
            // default behavior is to do nothing
        }

        @Override
        public String toString() {
            return name().toLowerCase();
        }
    }

    /** The XML attributes of the "throwables" element. */
    private enum ThrowablesAttributes {
        /** The {@code package} attribute of the {@code throwables} XML element. */
        PACKAGE,
        /** The {@code header} attribute of the {@code throwables} XML element. */
        HEADER,
        /** The {@code licensefile} attribute of the {@code throwables} XML element. */
        LICENSEFILE;

        @Override
        public String toString() {
            return name().toLowerCase();
        }
    }

    /** The XML attributes of the "exception" element. */
    private enum ExceptionAttributes {
        /** The {@code name} attribute of the {@code exception} XML element. */
        NAME,
        /** The {@code doc} attribute of the {@code exception} XML element. */
        DOC,
        /** The {@code supertype} attribute of the {@code exception} XML element. */
        SUPERTYPE,
        /** The {@code package} attribute of the {@code exception} XML element. */
        PACKAGE;

        @Override
        public String toString() {
            return name().toLowerCase();
        }
    }

    /** The XML attributes of the "subtype" element. */
    private enum SubTypeAttributes {
        /** The {@code name} attribute of the {@code subtype} XML element. */
        NAME;

        @Override
        public String toString() {
            return name().toLowerCase();
        }
    }

    /**
     * Produces the {@code ExceptionSpecification}s built by parsing the
     * configuration file.
     *
     * <p>Allow parsing to complete before calling this method.
     *
     * @return The {@code ExceptionSpecification}s built by parsing the
     *         configuration file. May be the empty set, but never {@code
     *         null}.
     *
     * @to.do
     *
     * <ul>
     *
     * <li>Throw {@code IllegalStateException} if this method is called before
     *     parsing completes.
     *
     * </ul>
     */
    public Collection<ExceptionSpecification> getExceptionSpecifications() {
        return exceptions;
    }

    @Override
    public void setDocumentLocator(final Locator parserGeneratedLocator) {
        assert initialized.compareAndSet(false, true);
        this.locator = parserGeneratedLocator;
    }

    @Override
    public void characters(final char[] ch, final int start, final int length) {
        assert initialized.get();
        final String characters = new String(ch, start, length);
        final String allSpacesFromBeginningToEndOfInput = "\\A\\s*\\z";
        final boolean allWhitespace = characters.matches(allSpacesFromBeginningToEndOfInput);
        if (! allWhitespace) {
            LOGGER.log(FINE, characters);
            if (readingCode) {
                currentException.setExtraCode(characters);
            }
        }
    }

    @Override
    public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
        assert initialized.get();
        final String qNameUpperCase = qName.toUpperCase();
        try {
            final Element element = Enum.valueOf(Element.class, qNameUpperCase);
            if (LOGGER.isLoggable(FINE)) {
                LOGGER.log(FINE, element.toString());
                for (int i = 0; i < attributes.getLength(); ++i) {
                    final String logMsg = attributes.getQName(i) + ": " + attributes.getValue(i);
                    LOGGER.log(FINE, logMsg);
                }
            }
            element.onStart(this, attributes);
        } catch (final IllegalArgumentException iae) {
            final String errMsg = MSGS.get(L7dMsg.SAX_UNKNOWN_ELEMENT, qName);
            final SAXException se = new SAXParseException(errMsg, locator);
            se.initCause(iae);
            throw se; // SuppressWarnings(PMD.PreserveStackTrace) justification: (false positive) exceptions are chained
        }
    }

    @Override
    public void endElement(final String uri, final String localName, final String qName) throws SAXException {
        assert initialized.get();
        final String qNameUpperCase = qName.toUpperCase();
        try {
            final Element element = Enum.valueOf(Element.class, qNameUpperCase);
            element.onEnd(this);
        } catch (final IllegalArgumentException iae) {
            final String errMsg = MSGS.get(L7dMsg.SAX_UNKNOWN_ELEMENT, qName);
            final SAXException se = new SAXParseException(errMsg, locator);
            se.initCause(iae);
            throw se; // SuppressWarnings(PMD.PreserveStackTrace) justification: (false positive) exceptions are chained
        }
    }

    /**
     * Sets the current {@link ExceptionSpecification} to a new {@link
     * ExceptionSpecification}.
     *
     * @param attributes
     *
     *        The values parsed from XML that are used to create a new {@code
     *        ExceptionSpecification}. May not be {@code null}.
     *
     * @param defaultClassName
     *
     *        The class name to use by default if one is no provided as the
     *        value of an attribute named {@code name}. May not be {@code
     *        null}.
     */
    private void newExceptionSpecification(final Attributes attributes, final String defaultClassName) {
        final String nameAttributeName = ExceptionAttributes.NAME.toString();
        final String className = attributes.getValue(nameAttributeName);

        final String packageAttributeName = ExceptionAttributes.PACKAGE.toString();
        final String packageName = attributes.getValue(packageAttributeName);
        final String effectivePackageName = (packageName == null) ? defaultPackageName : packageName;

        /*
         * TODO: Ensure that supertype is or is not a checked or unchecked
         * exception if this type is checked or uncheck, as
         * appropriate. Generate an error if constraint is violated.
         *
         * Consider whether any XML schema language has the power to perform
         * this validation. I don't think they do...
         */
        final String supertypeAttributeName = ExceptionAttributes.SUPERTYPE.toString();
        final String superClassName = attributes.getValue(supertypeAttributeName);
        final String effectiveSuperClassName = (superClassName == null) ? defaultClassName : superClassName;

        final String docAttributeName = ExceptionAttributes.DOC.toString();
        final String documentation = attributes.getValue(docAttributeName);
        final String effectiveDocumentation = (documentation == null) ? "" : documentation;

        final ExceptionSpecification exceptionSpecification = new ExceptionSpecification(className, effectivePackageName, effectiveSuperClassName, documentationHeader, license, effectiveDocumentation);
        currentException = exceptionSpecification;
    }


    /**
     * Adds the {@link ExceptionSpecification} {@link #currentException} to
     * the set of {@code ExceptionSpecification} held by this {@code
     * ConfigHandler}. {@link #currentException} is then set to {@code
     * null}. It will be set to a new {@link ExceptionSpecification} if
     * another {@link ExceptionSpecification} is described in the
     * configuration file.
     */
    @SuppressWarnings("PMD.NullAssignment") // justification: see javadoc for currentException
    private void clearExceptionSpecification() {
        assert (currentException != null);
        exceptions.add(currentException);
        currentException = null;
    }

    /**
     * Produces a string that has the contents of the specified {@link
     * java.io.File} with a space and an asterisk preceding each line.
     *
     * @param file
     *
     *        The contents of this file are produced.
     *
     * @throws IOException
     *
     *         If the contents of {@code file} cannot be produced.
     *
     * @return A string containing the contents of {@code file} suitable to be
     *         embedded in a Javadoc comment. Never {@code null}.
     */
    private String readFileContentsAsJavadoc(final File file) throws IOException {
        final FileReader fileReader = new FileReader(file);
        final BufferedReader reader = new BufferedReader(fileReader);
        try {
            final StringBuilder sb = new StringBuilder();
            while (true) {
                final String line = reader.readLine();
                if (line == null) {
                    break;
                }
                final boolean isEmptyString = (line.length() == 0);
                sb.append(isEmptyString ? " *" : " * "); // make ready for Javadoc
                sb.append(line);
                sb.append(LINE_SEPARATOR);
            }
            final String contents = sb.toString();
            return contents;
        } finally {
            reader.close();
        }
    }

    /**
     * Produces a string that has the contents of the named file with a space
     * and an asterisk preceding each line or the emtpy string if {@code
     * filename} is {@code null}.
     *
     * @param filename
     *
     *        The contents of the file named by this argument are
     *        produced. May be {@code null}, in which case the empty string is
     *        produced.
     *
     * @throws IOException
     *
     *         If the contents of the file named by {@code filename}
     *         cannot be produced.
     *
     * @return A string containing the contents of {@code filename}
     *         suitable to be embedded in a Javadoc comment. May be the empty
     *         string, but never {@code null}.
     */
    private String getFileContentsAsJavadoc(@Nullable final String filename) throws IOException {
        if (filename == null) {
            return "";
        }

        final File file = new File(filename);
        final String contents = readFileContentsAsJavadoc(file);
        return contents;
    }
}
